package im.composer.audio.engine;

import jass.engine.BufferNotAvailableException;

import java.util.logging.Logger;

import org.jaudiolibs.audioservers.AudioConfiguration;
import org.tritonus.share.sampled.AudioUtils;
import org.tritonus.share.sampled.FloatSampleBuffer;

/**
 * Output-only unit. Will produce audio-rate buffers. Needs only implementation of computeBuffer().
 * 
 * @author Kees van den Doel (kvdoel@cs.ubc.ca)
 */

public abstract class Out implements Source {

	private transient boolean started = false;
	private transient AudioConfiguration context;
	protected transient double millisecond_per_frame;
	/**
	 * The current time of this object, measured in number of frames of length bufferSize. So this is the number of frames processed.
	 */
	private transient long currentTime;
	private int sampleOffset = 0;

	/** Buffer length of processed audio buffers. */
	protected transient int bufferSize;

	/** The current buffer. */
	protected transient FloatSampleBuffer buf;

	/** The old buffer. */
	protected transient FloatSampleBuffer bufOld;

	/** To provide access to the old buffer without locking whole class */
	protected transient Object lock;

	protected BufferPlugin buf_plugin;

	protected void copyToOld() {
		if (lock == null) {
			lock = new Object();
		}
		synchronized (lock) {
			try {
				buf.copyTo(bufOld, 0, bufferSize);
			} catch (Exception e) {
			}
		}
	}

	/**
	 * Create at time 0 (which you may want to change by calling setTime() if objects are created in the middle of some jass.sis process).
	 */
	public Out(AudioConfiguration context) {
		setContext(context);
		setTime(0);
	}

	/**
	 * Create. Does not allocate bufferSize or set time which is still unknown yet if objects are created in the middle of some jass.sis process).
	 */
	public Out() {
	}

	public AudioConfiguration getContext() {
		return context;
	}

	/**
	 * 设置本输出单元的音频环境。此方法不抛出异常，应当于初始化本输出单元时调用。初始化后，音频环境若有变更，应当调用configure()方法。
	 * 
	 * @param context
	 */
	public void setContext(AudioConfiguration context) {
		this.context = context;
		if (context != null) {
			millisecond_per_frame = AudioUtils.frames2MillisD(context.getMaxBufferSize(), context.getSampleRate());
			setBufferSize(context.getMaxBufferSize());
			clearBuffer();
			if(buf_plugin!=null){
				buf_plugin.configure(context);
			}
		}
	}

	@Override
	public synchronized void configure(AudioConfiguration context) throws Exception {
		setContext(context);
	}

	/**
	 * Get current time.
	 * 
	 * @return current time.
	 */
	public long getTime() {
		return currentTime;
	}

	/**
	 * Set current time. Usually called only at init time.
	 * 
	 * @param t
	 *            current time.
	 */
	public synchronized void setTime(long t) {
		currentTime = t;
		if (t == 0) {
			started = false;
		}
		notify();
	}

	/**
	 * Set current time and notify waiting threads (used by ThreadMixer).
	 * 
	 * @param t
	 *            current time.
	 */
	public synchronized void setTimeAndNotify(long t) {
		currentTime = t;
		if (t == 0) {
			started = false;
		}
		notifyAll();
	}

	/**
	 * Reset time of self and all inputs
	 * 
	 * @param t
	 *            time to reset to. Patch must be in a state s.t. none of the current times == t
	 */
	public synchronized void resetTime(long t) {
		setTime(t);
	}

	/**
	 * Get buffer size.
	 * 
	 * @return buffer size in samples.
	 */
	public int getBufferSize() {
		return bufferSize;
	}

	/**
	 * Set buffer size. Will also reallocate buffer and clear.
	 * 
	 * @param bufferSize
	 *            buffer size.
	 */
	public void setBufferSize(int bufferSize) {
		this.bufferSize = bufferSize;
		int channels = getContext().getOutputChannelCount();
		if (buf == null) {
			buf = new FloatSampleBuffer(channels, bufferSize, context.getSampleRate());
		} else {
			buf.setSampleCount(bufferSize, true);
		}
		if (bufOld == null) {
			bufOld = new FloatSampleBuffer(channels, bufferSize, context.getSampleRate());
		} else {
			bufOld.setSampleCount(bufferSize, true);
		}
		clearBuffer();
	}

	public int getSampleOffset() {
		return sampleOffset;
	}

	public void setSampleOffset(int sampleOffset) {
		this.sampleOffset = sampleOffset;
	}

	/**
	 * Compute the next buffer and store in member float[] buf. This is the core processing method which will be implemented for each generator.
	 */
	protected abstract void computeBuffer();

	public final void setBufferPlugin(BufferPlugin buf_plugin) {
		this.buf_plugin = buf_plugin;
	}

	/**
	 * Clears buffer to zero.
	 */
	public void clearBuffer() {
		buf.makeSilence();
	}

	/**
	 * Get buffer with frame index t. Return old buffer if have it in cache. Compute next buffer and advance time if requested, throw exception if requested buffer lies in the past or future. This
	 * method will be called "behind the scenes" when processing filtergraphs.
	 * 
	 * @param t
	 *            timestamp of buffer = frame index.
	 */
	public synchronized FloatSampleBuffer getBuffer(long t) throws BufferNotAvailableException {
		copyToOld();
		if ((currentTime == 0 && !started) || t == currentTime + 1) { // requested next buffer
			setTime(t);
			prepareBuffer();
		} else if (t != currentTime) { // neither current or next buffer requested: deny request
			Logger.getAnonymousLogger().severe("Error! " + this + " Out.java: t=" + t + " currentTime=" + currentTime);
			throw new BufferNotAvailableException();
		}
		started = true;
		// return new or old buffer:
		return buf;
	}

	protected void prepareBuffer() {
		computeBuffer();
		if (buf_plugin != null) {
			buf_plugin.bufferReceived(buf, currentTime);
		}
	}

	/**
	 * Get old buffer in cache. Deliberately not synchronized.
	 */
	public FloatSampleBuffer getBuffer() throws BufferNotAvailableException {
		synchronized (lock) {
			return bufOld;
		}
	}

}
